Skip to content

Draft: Download manager #7

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 31 commits into
base: main
Choose a base branch
from
Open

Draft: Download manager #7

wants to merge 31 commits into from

Conversation

cyberphantom52
Copy link
Contributor

@cyberphantom52 cyberphantom52 commented Jul 20, 2025

Implements #6

TODO:

  • Retry with backoff
  • Cancel Download
  • Pause/Resume
  • Callbacks (Do we even need these)
  • Timeouts
  • Better progress tracking
  • DownloadManager stats(e.g active/queued/failed/completed downloads, total data downloaded)
  • ...

Comment on lines +11 to +12
#[error("Oneshot: {0}")]
Oneshot(#[from] tokio::sync::oneshot::error::RecvError),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like having to add this specific error type.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe, not sure, RecvError could map to a more general channel cancellation variant instead of adding a dedicated Oneshot error

edfloreshz and others added 2 commits July 20, 2025 18:31
- added missing impl for GPTK
- fixed warnings and clippy
This fixes a bug where if we didn't store the handle to the download somewhere it would automatically cancel the download
Comment on lines +71 to +75
let mut response = client
.get(req.url().as_ref())
.send()
.await?
.error_for_status()?;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want to use config.user_agent() to set the request’s User‑Agent header, here

status: status_tx,
cancel: cancel.clone(),
config,
last_progress_update: Instant::now(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be a good idea to initialize last_progress_update so the first InProgress status is sent immediately?

Comment on lines +26 to +34
for attempt in 0..=(max_attempts + 1) {
if attempt > max_attempts {
req.fail(Error::Download(DownloadError::RetriesExhausted {
last_error_msg: last_error
.as_ref()
.map(ToString::to_string)
.unwrap_or_else(|| "Unknown Error".to_string()),
}));
return;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe consider iterating 0..=max_attempts and handling the exhausted case when attempt == max_attempts

Comment on lines +39 to +41
// Basic exponential backoff
let delay_ms = 1000 * 2u64.pow(attempt as u32 - 1);
let delay = Duration::from_millis(delay_ms);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe cap the exponential backoff and sprinkle in some jitter so retries don’t wait forever.

Comment on lines +26 to +52
for attempt in 0..=(max_attempts + 1) {
if attempt > max_attempts {
req.fail(Error::Download(DownloadError::RetriesExhausted {
last_error_msg: last_error
.as_ref()
.map(ToString::to_string)
.unwrap_or_else(|| "Unknown Error".to_string()),
}));
return;
}

if attempt > 0 {
req.retry();
// Basic exponential backoff
let delay_ms = 1000 * 2u64.pow(attempt as u32 - 1);
let delay = Duration::from_millis(delay_ms);

tokio::select! {
_ = tokio::time::sleep(delay) => {},
_ = req.cancel.cancelled() => {
req.cancel();
return;
}
}
}

match download(client.clone(), &mut req).await {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A quick check if the request is canceled at the start of each attempt might let us exit before starting the network request

Comment on lines +91 to +93
drop(file); // Manually drop the file handle to ensure that deletion doesn't fail
tokio::fs::remove_file(&req.destination()).await?;
return Err(Error::Download(DownloadError::Cancelled));
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps ignore any errors from remove_file so cancellation doesn’t hide the real problem

Same at L106‑108

Comment on lines +86 to +88
pub fn active_downloads(&self) -> usize {
// -1 because the dispatcher thread is always running
self.tracker.len() - 1
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using saturating_sub(1) could keep active_downloads from goinig negative if the dispatcher isn't running

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants